﻿#region [ License information          ]
/* ************************************************************
 *
 * Copyright (c) Bjørn Bouet Smith, 2010, 2011
 *
 * This source code is subject to terms and conditions of 
 * Microsoft Public License (Ms-PL).
 * 
 * A copy of the license can be found in the license.txt
 * file at the root of this distribution. If you can not 
 * locate the License, please send an email to bjornsmith@gmail.com
 * 
 * By using this source code in any fashion, you are 
 * agreeing to be bound by the terms of the Microsoft 
 * Permissive License.
 *
 * You must not remove this notice, or any other, from this
 * software.
 *
 * ************************************************************/
#endregion

using System;
using System.Collections.Generic;
using System.Configuration;
using Smith.AMC.Configuration;
using Smith.AMC.Network;
using Smith.AMC.Operations;
using Smith.AMC.Serialization;
using Wintellect.Threading.AsyncProgModel;

namespace Smith.AMC
{
    /// <summary>
    /// Implementation of the <see cref="IMemcachedClient"/> interface.
    /// </summary>
    public class MemcachedClient : IMemcachedClient
    {
        private readonly ISocketWorker socketWorker;

        /// <summary>
        /// The serializer that is used to serialize/deserialize items
        /// </summary>
        private readonly ItemSerializer itemSerializer = new ItemSerializer();

        /// <summary>
        /// Initializes a new instance of the <see cref="MemcachedClient"/> class.
        /// </summary>
        /// <param name="socketWorker">The worker for processing commands against the memcached servers</param>
        public MemcachedClient(ISocketWorker socketWorker)
        {
            this.socketWorker = socketWorker;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MemcachedClient"/> class.
        /// </summary>
        /// <param name="configuration">The configuration to use for this instance.</param>
        public MemcachedClient(MemcachedClientConfiguration configuration)
            : this(new SocketWorker(new ServerPool(configuration), TimeSpan.FromSeconds(configuration.ConnectionTimeout)))
        {
            if (configuration == null)
            {
                throw new ArgumentNullException("configuration");
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MemcachedClient"/> class.
        /// Will load default configuration from the App.Config.
        /// </summary>
        public MemcachedClient()
            : this(
            (MemcachedClientConfiguration)ConfigurationManager.GetSection("smith.amc"))
        {
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            socketWorker.Dispose();
        }

        /// <summary>
        /// Begins an asynchronous add operation against the memcached server.
        /// This operation will only add the item to the cache if it does not already exist
        /// </summary>
        /// <typeparam name="T">The type of data to add.</typeparam>
        /// <param name="cacheItem">The item to cache.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginAdd operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginAdd<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new AddOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous add operation
        /// </summary>
        /// <typeparam name="T">Type of item added</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the add operation was a success; false otherwise
        /// </returns>
        public bool EndAdd<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous replace operation against the memcached server.
        /// This operation will only replace the item in the cache not already exist
        /// </summary>
        /// <typeparam name="T">The type of data to replace.</typeparam>
        /// <param name="cacheItem">The item to replace.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginReplace operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginReplace<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new ReplaceOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous replace operation
        /// </summary>
        /// <typeparam name="T">The type of data to replace</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the replace operation was a success: false otherwise
        /// </returns>
        public bool EndReplace<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous remove operation against the memcached server.
        /// This operation will only remove the item in the cache if it exists.
        /// No exceptions will be thrown if the item does not exist
        /// </summary>
        /// <param name="cacheKey">The cacheKey of the item to replace.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginRemove operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginRemove(string cacheKey, AsyncCallback callback, object state)
        {
            Operation operation = new DeleteOperation
            {
                Key = cacheKey
            };
            return socketWorker.BeginSocketOperation<string>(operation, callback, state);

        }

        /// <summary>
        /// Handles the end of the asynchronous remove operation
        /// </summary>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the remove operation was a success; false otherwise
        /// </returns>
        public bool EndRemove(IAsyncResult asyncResult)
        {
            OperationResponse<string> operationResponse = socketWorker.EndSocketOperation<string>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous get operation against the memcached server.
        /// No exceptions will be thrown if the item does not exist
        /// </summary>
        /// <typeparam name="T">the type of data to replace</typeparam>
        /// <param name="cacheKey">The cacheKey of the item to replace.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginGet<T>(string cacheKey, AsyncCallback callback, object state)
        {
            GetOperation operation = new GetOperation
            {
                Key = cacheKey
            };
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous remove operation
        /// </summary>
        /// <typeparam name="T">The type of item to retrieve.</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// A cache item with the cached value; or null if no item could be found
        /// with the cachekey given in the BeginGet operation
        /// </returns>
        public ICacheItem<T> EndGet<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);

            CacheItem<T> item = new CacheItem<T>
                                    {
                                        CacheKey = operationResponse.CacheKey,
                                        CASValue = operationResponse.CASValue,
                                        Value = OperationResponseDecoder.DecodeResponse(operationResponse)
                                    };

            return item;
        }

        /// <summary>
        /// Begins an asynchronous gets operation against the memcached server.
        /// The difference between get and gets is that gets will return the CAS value for the cache item.
        /// So only use this method if you intend to use the CAS value for a CAS operation.
        /// No exceptions will be thrown if the item does not exist
        /// </summary>
        /// <typeparam name="T">the type of data to replace</typeparam>
        /// <param name="cacheKey">The cacheKey of the item to replace.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginGets<T>(string cacheKey, AsyncCallback callback, object state)
        {
            GetsOperation operation = new GetsOperation
                                          {
                                              Key = cacheKey
                                          };
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous gets operation
        /// </summary>
        /// <typeparam name="T">The type of item to retrieve.</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// A cache item with the cached value; or null if no item could be found
        /// with the cachekey given in the BeginGets operation
        /// </returns>
        public ICacheItem<T> EndGets<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);

            T response = OperationResponseDecoder.DecodeResponse(operationResponse);
            CacheItem<T> item = new CacheItem<T>
            {
                Value = response,
                CacheKey = operationResponse.CacheKey,
                CASValue = operationResponse.CASValue
            };

            return item;
        }

        /// <summary>
        /// Begins an asynchronous append operation against the memcached server.
        /// </summary>
        /// <typeparam name="T">The type of data to append to the item if found in the cache.</typeparam>
        /// <param name="cacheItem">The cache item.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginAppend<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new AppendOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous append operation
        /// </summary>
        /// <typeparam name="T">The type of data to append</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the append operation was a success; false otherwise
        /// </returns>
        public bool EndAppend<T>(IAsyncResult asyncResult)
        {
            OperationResponse<string> operationResponse = socketWorker.EndSocketOperation<string>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous prepend operation against the memcached server.
        /// </summary>
        /// <typeparam name="T">The type of data to prepend to the item if found in the cache.</typeparam>
        /// <param name="cacheItem">The cache item.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginPreAppend<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new PrependOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous prepend operation
        /// </summary>
        /// <typeparam name="T">The type of data to prepend.</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the prepend operation was a success; false otherwise
        /// </returns>
        public bool EndPreAppend<T>(IAsyncResult asyncResult)
        {
            OperationResponse<string> operationResponse = socketWorker.EndSocketOperation<string>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous multi Check and Set operation against the memcached server.
        /// Check and Set means replace the item in the cache with this item, except if someone else
        /// modified it in the mean time.
        /// CASValue must be set on the cacheItem, otherwise this operation will fail
        /// </summary>
        /// <typeparam name="T">The type of data the cache item represents</typeparam>
        /// <param name="cacheItem">The cache item to replace.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginCheckAndSet<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new CASOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime,
                CasValue = cacheItem.CASValue
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous Check and Set operation
        /// </summary>
        /// <typeparam name="T">The type of data to check and set</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the cas operation was a success; false otherwise
        /// </returns>
        public bool EndCheckAndSet<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous multi get operation against the memcached server.
        /// This operation will return a collection of cache items with the items that existed in the cache.
        /// </summary>
        /// <typeparam name="T">The type of data to retrieve.
        /// All cache keys should represent data of the same type</typeparam>
        /// <param name="cacheKeys">The cache keys of the items to retrieve.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginGet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginGetMultiple<T>(ICollection<string> cacheKeys, AsyncCallback callback, object state)
        {
            AsyncEnumerator<ICollection<ICacheItem<T>>> ae = new AsyncEnumerator<ICollection<ICacheItem<T>>>();
            return ae.BeginExecute(DoGetMultiple(ae, cacheKeys), callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous multi get operation
        /// </summary>
        /// <typeparam name="T">The type of data to get</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// A collection with the items found in the cache; if no items was found an empty collection is returned
        /// </returns>
        public ICollection<ICacheItem<T>> EndGetMultiple<T>(IAsyncResult asyncResult)
        {
            return AsyncEnumerator<ICollection<ICacheItem<T>>>.FromAsyncResult(asyncResult).EndExecute(asyncResult);
        }

        private IEnumerator<int> DoGetMultiple<T>(AsyncEnumerator<ICollection<ICacheItem<T>>> ae, ICollection<string> cacheKeys)
        {
            ICollection<ICacheItem<T>> results = new List<ICacheItem<T>>();

            foreach (string cacheKey in cacheKeys)
            {
                GetOperation operation = new GetOperation
                {
                    Key = cacheKey
                };

                socketWorker.BeginSocketOperation<T>(operation, ae.End(), null);
                yield return 1;
            }

            foreach (string cacheKey in cacheKeys)
            {
                
                    OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(ae.DequeueAsyncResult());

                    CacheItem<T> item = new CacheItem<T>
                                            {
                                                CacheKey = operationResponse.CacheKey,
                                                CASValue = operationResponse.CASValue,
                                                Value = OperationResponseDecoder.DecodeResponse(operationResponse)
                                            };
                    results.Add(item);
            }
            ae.Result = results;
        }

        /// <summary>
        /// Begins an asyncronous operation for setting the initial value of a cache key.
        /// This operation should be called prior to using either Increment or Decrement.
        /// </summary>
        /// <param name="cacheKey">The cache key.</param>
        /// <param name="initialValue">The initial value to set in the server.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginSetValueForIncrement operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginSetValueForIncrement(string cacheKey, ulong initialValue, AsyncCallback callback, object state)
        {
            SetOperation operation = new SetOperation
            {
                Key = cacheKey
            };
            
            itemSerializer.Serialize(typeof(string), Convert.ToString(initialValue), operation);

            return socketWorker.BeginSocketOperation<string>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous operation for setting the initial value for a cache key
        /// </summary>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the initial setting of the value was a success; false otherwise
        /// </returns>
        public bool EndSetValueForIncrement(IAsyncResult asyncResult)
        {
            OperationResponse<string> operationResponse = socketWorker.EndSocketOperation<string>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }

        /// <summary>
        /// Begins an asyncronous increment operation.
        /// </summary>
        /// <param name="cacheKey">The cache key.</param>
        /// <param name="incrementBy">The value to increment the existing value by.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginIncrement operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginIncrement(string cacheKey, ulong incrementBy, AsyncCallback callback, object state)
        {
            IncrementOperation operation = new IncrementOperation
            {
                Key = cacheKey,
                Data = incrementBy
            };
            return socketWorker.BeginSocketOperation<ulong>(operation,callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous increment operation.
        /// </summary>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// The incremented value from the cache; or 0 (zero) if no existing key could be found
        /// </returns>
        public ulong EndIncrement(IAsyncResult asyncResult)
        {
            OperationResponse<ulong> operationResponse = socketWorker.EndSocketOperation<ulong>(asyncResult);
            return OperationResponseDecoder.DecodeUlongResponse(operationResponse);
        }

        /// <summary>
        /// Begins an asyncronous decrement operation.
        /// </summary>
        /// <param name="cacheKey">The cache key.</param>
        /// <param name="decrementBy">The value to decrement the existing value by.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginIncrement operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginDecrement(string cacheKey, ulong decrementBy, AsyncCallback callback, object state)
        {
            DecrementOperation operation = new DecrementOperation
            {
                Key = cacheKey,
                Data = decrementBy
            };
            return socketWorker.BeginSocketOperation<ulong>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous decrement operation.
        /// </summary>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// The decremented value from the cache; or 0 (zero) if no existing key could be found
        /// </returns>
        public ulong EndDecrement(IAsyncResult asyncResult)
        {
            OperationResponse<ulong> operationResponse = socketWorker.EndSocketOperation<ulong>(asyncResult);
            return OperationResponseDecoder.DecodeUlongResponse(operationResponse);
        }

        /// <summary>
        /// Begins an asynchronous set operation against the memcached server.
        /// This operation will unconditionally set the value, no matter if there is already another values present.
        /// </summary>
        /// <typeparam name="T">The type of data to set</typeparam>
        /// <param name="cacheItem">The item to cache.</param>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginSet operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginSet<T>(ICacheItem<T> cacheItem, AsyncCallback callback, object state)
        {
            Operation operation = new SetOperation
            {
                Key = cacheItem.CacheKey,
                Expiry = cacheItem.LifeTime
            };

            itemSerializer.Serialize(typeof(T), cacheItem.Value, operation);
            return socketWorker.BeginSocketOperation<T>(operation, callback, state);
        }

        /// <summary>
        /// Handles the end of the asynchronous set operation
        /// </summary>
        /// <typeparam name="T">Type of item set</typeparam>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        /// <returns>
        /// true if the set operation was a success; false otherwise
        /// </returns>
        public bool EndSet<T>(IAsyncResult asyncResult)
        {
            OperationResponse<T> operationResponse = socketWorker.EndSocketOperation<T>(asyncResult);
            return OperationResponseDecoder.IsSuccess(operationResponse);
        }
    }
}
